Netty零拷贝(ZeroCopy) 您所在的位置:网站首页 netty 直接内存 Netty零拷贝(ZeroCopy)

Netty零拷贝(ZeroCopy)

2023-08-16 03:10| 来源: 网络整理| 查看: 265

前言

在Java技术栈中,Netty一直以来都作为网络编程的不二之选。Netty是基于Java Nio封装的网络编程框架,具有高并发、传输速度快、封装友好、扩展性强等特点。本文就着重分析下Netty传输速度快特点的技术方案:零拷贝

操作系统 传统数据拷贝 在这里插入图片描述

由上图可以看出,传统的数据拷贝方式分为以下4步。 1 将数据从磁盘读取到内核缓存 2 将数据从内核缓存读取到用户缓存 3 将数据从用户缓存写入到socket缓存 4 将数据从socket缓存写入到网卡设备 其中1、4步骤是有DMA(见文尾) COPY完成,2、3是有CPU COPY完成 其中2、3步骤是由CPU完成的,因为拷贝是将文件切分成多段,如果遇到较大文件,cpu的成本开销是巨大的,也会阻塞其他进程。所以,在一些场景下,例如不需要在用户缓存对数据进行校验或者修改等操作,是没有必要进行2、3步骤的,可以节省大量的cpu资源。

linux2.1内核 在这里插入图片描述

去掉了传统模式的2、3步骤,不再需要将数据在内核缓存和用户缓存中来回拷贝,节省的cpu上下文切换的成本。

linux2.4内核 在这里插入图片描述

优化了linux2.1之前的 将数据从ReadBuffer copy到Socketbuffer 的动作,变成 将数据的位置和长度的信息的描述符被追加到了SocketBuffer中,DMA引擎直接把数据从内核缓冲区传输到协议引擎 。真正实现了零拷贝。

Java NIO

Java NIO提供了transferTo() 方法依赖于linux操作系统的sendfile方法完成的,所以在不同的linux版本中,正如上面所述,实现是不同的。

Netty

代码分析基于netty4.1.25.final版本

操作系统层面 DefaultFileRegion类中transferTo()方法,就是调用的Java NIO的transferTo() 方法,完成了对操作系统层面数据零拷贝的支持。 @Override public long transferTo(WritableByteChannel target, long position) throws IOException { long count = this.count - position; if (count < 0 || position < 0) { throw new IllegalArgumentException( "position out of range: " + position + " (expected: 0 - " + (this.count - 1) + ')'); } if (count == 0) { return 0L; } if (refCnt() == 0) { throw new IllegalReferenceCountException(0); } // Call open to make sure fc is initialized. This is a no-oop if we called it before. open(); long written = file.transferTo(this.position + position, count, target); if (written > 0) { transferred += written; } return written; }

用户态(Java层面) Heap Buffer堆缓冲区是JVM堆中的空间,每次读写数据都要先将数据拷贝到直接缓冲区再进行传递,而Direct Buffer直接缓冲区是JVM实现通过本地调用来分配内存,在堆之外直接分配内存,直接缓冲区不会占用堆的容量,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中,相对于Heap Buffer则节省了一次拷贝的过程。

通过wrap操作, 我们可以将byte[]、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象, 进而避免拷贝操作。以byte[]零拷贝为例,普通拷贝方式是新创建一个byteBuf,默认容量256字节,再把byte数组拷贝到butyBuf中。而零拷贝则只是创建一个对数组的引用,没有开辟新的内存,优势明显。

@Test public void byteToByteBuf(){ String a = "ccc"; byte[] bytesSrc = a.getBytes(CharsetUtil.UTF_8); //普通拷贝 ByteBuf byteBuf = Unpooled.buffer(); byteBuf.writeBytes(bytesSrc); // 零拷贝 ByteBuf byteBufZero = Unpooled.wrappedBuffer(bytesSrc); }

Netty提供CompositeByteBuf复合缓冲区类, 可以将多个ByteBuf合并为一个逻辑上的ByteBuf, 避免了各个ByteBuf之间的拷贝。假设有一份协议数据,它由头部和消息体组成,而头部和消息体是分别存放在两个ByteBuf中的, 为了方便后续处理,要将两个ByteBuf进行合并

@Test public void compositeByteBufTest(){ String header = "ccc"; String body = "dddd"; ByteBuf buf1 = Unpooled.wrappedBuffer(header.getBytes(CharsetUtil.UTF_8)); ByteBuf buf2 = Unpooled.wrappedBuffer(body.getBytes(CharsetUtil.UTF_8)); ByteBuf compositeByteBuf = Unpooled.wrappedBuffer(buf1,buf2); int size = compositeByteBuf.readableBytes(); byte[] bytes = new byte[size]; compositeByteBuf.readBytes(bytes); String value = new String(bytes,CharsetUtil.UTF_8); System.out.println("composite buff result : " + value); }

在这里插入图片描述

ByteBuf支持slice操作,可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免内存的拷贝。

@Test public void sliceTest() { String str = "cccdddd"; ByteBuf buf = Unpooled.wrappedBuffer(str.getBytes(CharsetUtil.UTF_8)); ByteBuf slice = buf.slice(0, 3); int size = slice.readableBytes(); byte[] bytes = new byte[size]; slice.readBytes(bytes); String header = new String(bytes, CharsetUtil.UTF_8); System.out.println(header); }

在这里插入图片描述

Kafka

我们都了解到,kafka因为通过磁盘的顺序读写,才实现了高吞吐量,其实还有一个重要原因就是使用了零拷贝,调用的也是Java NIO中的transferTo()方法。由于kafka对数据不需要校验和修改,简直是零拷贝最完美的应用场景。

扩展知识 DMA(Direct Memory Access) 直接内存访问,是一种无需CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而大大提高系统的吞吐率。DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发的执行其他任务。当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,由CPU执行相应的中断服务程序进行后续处理。


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有